/*
	Some code logic was copied from Open Market FastCGI C++ library (fcgi-2.4.0) - file "os_win32.c".
	To avoid licensing issues attached original license file - "LICENSE.TERMS".
*/

#ifdef WIN32

#include <assert.h>
#include <process.h>
#include <cstdlib>
#include <boost/scoped_array.hpp>


#include "aconnect/util.hpp"
#include "aconnect/util.network.hpp"

#include "fastcgi_application_manager.hpp"

namespace fastcgi 
{ 
	namespace os
	{	
		const std::string BindPipePathPrefix = "\\\\.\\pipe\\FastCGI\\";

		// Win32 related
		const int TerminateTimeout = 5000; // msec
		
		aconnect::string_constant FcgiNamedPipeNameEnvVariable = "_FCGI_X_PIPE_";
		aconnect::string_constant ShutdownEventEnvVarName = "_FCGI_SHUTDOWN_EVENT_";
		
		static aconnect::string_constant ShutdownEventName = "Global\\AhttpFastCGIShutdownEvent";
		static HANDLE ShutdownEvent = INVALID_HANDLE_VALUE;
	};
	
	//==================================================================//
	//	Common ApplicationManager members								//
	//==================================================================//

	int ApplicationManager::createLocalIpcFileDescriptor (ConnectionMode mode, 
			int readWriteTimeout,
			int desiredIndex,
			aconnect::string_constptr path,
			aconnect::port_type port, 
			int backLog) throw (fastcgi_error)
	{
		int index = -1;
		
		SocketDescriptor desk;
		desk.mode = mode;
		
		if (mode == ConnectionTcpSocket) 
		{
			assert (port != 0);

			struct sockaddr_in	sockAddr;
			int sockLen = sizeof(sockAddr);
	        
			memset(&sockAddr, 0, sizeof(sockAddr));
			sockAddr.sin_family = AF_INET;
			sockAddr.sin_addr.s_addr = htonl(INADDR_ANY);
			sockAddr.sin_port = htons(port);

			SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);
			if (listenSock == INVALID_SOCKET) 
				throw fastcgi_error(::WSAGetLastError(), "createLocalIpcFileDescriptor: TCP socket creation failed.");
	        
			aconnect::util::setSocketReadTimeout (listenSock, readWriteTimeout);
			aconnect::util::setSocketWriteTimeout (listenSock, readWriteTimeout);

			if (0 != bind(listenSock, (struct sockaddr *) &sockAddr, sockLen)  )
				throw fastcgi_error(::WSAGetLastError(), "createLocalIpcFileDescriptor: TCP socket binding failed.");
			
			if (0 != listen(listenSock, backLog)) 
				throw fastcgi_error(::WSAGetLastError(), "createLocalIpcFileDescriptor: TCP socket listening  setup failed.");

			desk.sock = listenSock;
			index = registerConnection(desk, port, 0, desiredIndex);
	        
			if (index == -1)  {
				closesocket (listenSock);
				throw fastcgi_error(0, "createLocalIpcFileDescriptor: registerConnection failed for listening socket.");
			}
		}
		else
		{
			assert (path != 0 && path[0] != '\0');

			std::string pipePath = os::BindPipePathPrefix + path;

			HANDLE handleListenPipe = ::CreateNamedPipeA( (LPCSTR) pipePath.c_str(),
					PIPE_ACCESS_DUPLEX,
					PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_READMODE_BYTE,
					PIPE_UNLIMITED_INSTANCES,
					os::PipeBufferSize, 
					os::PipeBufferSize, 
					os::DefaultPipeWaitTimeout, 
					NULL);
	        
			if (handleListenPipe == INVALID_HANDLE_VALUE)
				throw fastcgi_error(::GetLastError(), "createLocalIpcFileDescriptor: CreateNamedPipe failed.");

			if ( !::SetHandleInformation(handleListenPipe, HANDLE_FLAG_INHERIT, TRUE))
				throw fastcgi_error(::GetLastError(), "createLocalIpcFileDescriptor: SetHandleInformation for named pipe failed.");
			
			desk.fileHandle = handleListenPipe;
			index = registerConnection(desk, 0, pipePath.c_str(), desiredIndex);
	        
			if (index == -1) 
			{
				::CloseHandle(handleListenPipe);
				throw fastcgi_error(0, "CreateLocalIpcFd: registerConnection failed for named pipe.");
			}
		}

		return index;
	}

	//==================================================================//
	SocketDescriptor ApplicationManager::connectToFcgiApplication (const ApplicationConnection &con, int readWriteTimeout) throw (fastcgi_error)
	{
		SocketDescriptor res;
		res.mode = con.sid.mode;

		ConnectionMode mode = con.sid.mode;
	    
		if (mode == ConnectionTcpSocket) 
		{
			struct sockaddr_in sockAddr;
			aconnect::util::zeroMemory (&sockAddr, sizeof( sockAddr ));

			struct hostent *hp = gethostbyname(aconnect::network::LocalhostName);
			if (hp == NULL)
				throw fastcgi_error (0, "connectToFcgiApplication: cannot resolve localhost name.");

			sockAddr.sin_family = AF_INET;
			sockAddr.sin_port = htons ( con.port );
			memcpy(&sockAddr.sin_addr, hp->h_addr, hp->h_length);

			int sockLen = sizeof (sockAddr);
			SOCKET clientSock = aconnect::util::createSocket (AF_INET);
			
			aconnect::util::setSocketReadTimeout (clientSock, readWriteTimeout);
			aconnect::util::setSocketWriteTimeout (clientSock, readWriteTimeout);

			if (0 != connect(clientSock, (struct sockaddr *) &sockAddr, sockLen)) {
				
				if (_log) _log->error (aconnect::socket_error(clientSock, "connectToFcgiApplication: connect() on TCP socket failed."));
				
				aconnect::util::closeSocket(clientSock);
				throw fastcgi_error (FcgiError::ClientConnectionFailed, "connectToFcgiApplication: connect() on TCP socket failed.");
			}

			res.sock = clientSock;
		}
		else if (mode == ConnectionPipe) 
		{
			HANDLE pipe = ::CreateFileA(con.path.c_str(),
					GENERIC_WRITE | GENERIC_READ,
					0,
					NULL,
					OPEN_EXISTING,
					0, // FILE_FLAG_OVERLAPPED
					NULL);

			if( pipe == INVALID_HANDLE_VALUE) 
				throw fastcgi_error (FcgiError::ClientConnectionFailed, "connectToFcgiApplication: CreateFile for named pipe failed");
			
			DWORD dwMode = PIPE_TYPE_BYTE | PIPE_WAIT | PIPE_READMODE_BYTE; 
			BOOL apiRes = ::SetNamedPipeHandleState(pipe,
				&dwMode,
				NULL,
				NULL);
			
			if (apiRes == 0) {
				::CloseHandle (pipe);
				throw fastcgi_error (::GetLastError(), "connectToFcgiApplication: SetNamedPipeHandleState for client pipe failed");
			}
			
			res.fileHandle = pipe;

			/*
			// while (err == ERROR_PIPE_BUSY)
			#ifdef _DEBUG
						const DWORD waitTimeout = 300 * 1000;
			#else
						const DWORD waitTimeout = os::DefaultPipeWaitTimeout;
			#endif
						apiRes = ::WaitNamedPipeA (con.path.c_str(), waitTimeout);
						if (apiRes == 0) {
							::CloseHandle (pipe);
							throw fastcgi_error (::GetLastError(), "connectToFcgiApplication: WaitNamedPipe timed out");
						}
			*/
			
			// INVESTIGATE: Create the I/O completion port to be used there for our I/O queue.
			/*
			if (!::CreateIoCompletionPort(pipe, HandleIoCompPort, descriptor, 1))
			{
				freeConnection (descriptor);
				::CloseHandle (pipe);
				throw fastcgi_error(0, "connectToFcgiApplication: CreateIoCompletionPort failed for named pipe.");
			}
			*/
		} else {
			throw fastcgi_error (FcgiError::InvalidConnectionInfo, "ApplicationManager::connectToFcgiApplication: unsupported application connection mode.");
		}

		return res;
	}

	//==================================================================//
	void ApplicationManager::close (int descriptor) throw (fastcgi_error)
	{
		ApplicationConnection con = getConnection(descriptor);

		assert (con.sid.mode != fastcgi::UnusedConnection);
		
		if (con.sid.mode == fastcgi::ConnectionTcpSocket) {
			SOCKET sock = con.sid.sock;
			assert (sock != INVALID_SOCKET);
			aconnect::util::closeSocket(sock);
		
		} else if (con.sid.mode == fastcgi::ConnectionPipe) {
			::CloseHandle (con.sid.fileHandle);
			con.sid.fileHandle = INVALID_HANDLE_VALUE;
		}
	}

	//==================================================================//
	void ApplicationManager::closeDescriptor (SocketDescriptor sd) throw (fastcgi_error)
	{
		assert (sd.sock != INVALID_SOCKET);
				
		if (sd.mode == fastcgi::ConnectionTcpSocket) {
			
			// shutdown() the send side and then read() from client until EOF
			// or a timeout expires.  This is done to minimize the potential
			// that a TCP RST will be sent by our TCP stack in response to 
			// receipt of additional data from the client.  The RST would
			// cause the client to discard potentially useful response data.
			if (shutdown(sd.sock, SD_SEND) == 0) {

				fd_set rfds;
				FD_ZERO(&rfds);
				FD_SET(sd.sock, &rfds);

				struct timeval tv = {2, 0};
				int rv;
				char trash[1024];
   				do {
					rv = select(sd.sock + 1, &rfds, NULL, NULL, &tv);
				} while (rv > 0 && recv(sd.sock, trash, sizeof(trash), 0) > 0);
			
			} else {
				throw fastcgi_error (::WSAGetLastError(), "ApplicationManager::Close: shutdown() failed.");
			}
			        
			aconnect::util::closeSocket(sd.sock);
		
		} else if (sd.mode == fastcgi::ConnectionPipe) {
			::CloseHandle (sd.fileHandle);
		}
	}

	//==================================================================//
	void ApplicationManager::write(const SocketDescriptor& sd, const byte_type* data, size_t dataLength, bool flush) throw (fastcgi_error)
	{
		assert (sd.mode != fastcgi::UnusedConnection);

		if (sd.mode == ConnectionPipe) {

			DWORD bytesWritten;		
			if (0 == ::WriteFile(sd.fileHandle, data, dataLength, &bytesWritten, NULL)) 
				throw fastcgi_error (::GetLastError(), "ApplicationManager::Write: writing to pipe failed.");

			assert ((size_t)bytesWritten == dataLength);

			if (flush)
				::FlushFileBuffers(sd.fileHandle); // eat error
		
		} else if (sd.mode == ConnectionTcpSocket) {

			int writtenDataLen = send(sd.sock, (const char*) data, dataLength, 0);

			if (SOCKET_ERROR == writtenDataLen) 
				throw fastcgi_error (::WSAGetLastError(), "ApplicationManager::Write: writing to TCP socket failed.");

			assert ((size_t) writtenDataLen == dataLength);

		} else {
			throw fastcgi_error (::GetLastError(), "ApplicationManager::Write: unsupported application connection mode.");
		}

	}

	//==================================================================//
	size_t ApplicationManager::read(const SocketDescriptor& sd, 
		byte_type* buffer, size_t bufferSize, 
		int sockectSelectTimeout) throw (fastcgi_error)
	{
		size_t readBytes = 0;
		
		assert (sd.mode != fastcgi::UnusedConnection);

		if (sd.mode == ConnectionPipe) {
			DWORD bytesRead = 0;
			
			if (0 == ::ReadFile(sd.fileHandle, buffer, bufferSize, &bytesRead, NULL)) 
			{
				errno_t errCode = ::GetLastError();
				if (errCode != ERROR_MORE_DATA)
					throw fastcgi_error (FcgiError::ReadFailed, "ApplicationManager::read: reading from pipe failed, error: 0x%08X.", errCode);
			} 

			readBytes = bytesRead;
		
		} else if (sd.mode == ConnectionTcpSocket) {

			fd_set rfds;
			FD_ZERO(&rfds);
			FD_SET(sd.sock, &rfds);

			struct timeval tv = {sockectSelectTimeout, 0};
			int rv = select(sd.sock + 1, &rfds, 0, 0, &tv);

			if (rv == SOCKET_ERROR)
				throw aconnect::socket_error (sd.sock, "ApplicationManager::read: select() for read from TCP socket failed.");
			
			if (rv > 0) {
				readBytes = recv(sd.sock, (char*) buffer, bufferSize, 0);
				if ((size_t) SOCKET_ERROR == readBytes) 
					throw fastcgi_error (::WSAGetLastError(), "ApplicationManager::read: read from TCP socket failed.");
			}
		
		} else {
			throw fastcgi_error (::GetLastError(), "ApplicationManager::isReadyForRead: unsupported application connection mode.");
		}

		return readBytes;
	}

	//==================================================================//
	bool ApplicationManager::isReadyForRead(const SocketDescriptor& sd, int sockectSelectTimeout) throw (fastcgi_error)
	{
		assert (sd.mode != fastcgi::UnusedConnection);

		if (sd.mode == ConnectionPipe) {
			
			DWORD bytesRead = 0;
			
			BOOL apiRes = ::PeekNamedPipe(sd.fileHandle, NULL, 0, NULL, &bytesRead, NULL);
			if (0 == apiRes)
				throw fastcgi_error (::GetLastError(), "ApplicationManager::isReadyForRead: PeekNamedPipe failed.");

			if (bytesRead > 0)
				return true;
		
		} else if (sd.mode == ConnectionTcpSocket) {

			fd_set rfds;
			FD_ZERO(&rfds);
			FD_SET(sd.sock, &rfds);

			struct timeval tv = {sockectSelectTimeout, };
			
			int rv = select(sd.sock + 1, &rfds, 0, 0, &tv);

			if (rv == SOCKET_ERROR)
				throw aconnect::socket_error (sd.sock, "ApplicationManager::isReadyForRead: select() for read from TCP socket failed.");
			
			if (rv > 0)
				return true;
	
		} else {
			throw fastcgi_error (::GetLastError(), "ApplicationManager::isReadyForRead: unsupported application connection mode.");
		}

		return false;
	}

	//==================================================================//
	aconnect::process_id_type ApplicationManager::spawnWorkerProcess (aconnect::string_constptr execPath, int descriptor, 
		aconnect::string_constptr commandLine)  throw (fastcgi_error)
	{
		assert (execPath && execPath[0] != '\0');
		ApplicationConnection con = getConnection(descriptor);

		assert (con.sid.mode != fastcgi::UnusedConnection);

		STARTUPINFOA StartupInfo;
		PROCESS_INFORMATION pInfo;
		
		memset((void *)&StartupInfo, 0, sizeof(STARTUPINFOA));
		StartupInfo.cb = sizeof (STARTUPINFO);

		if (con.sid.mode == fastcgi::ConnectionPipe) {
			if (0 == ::SetEnvironmentVariableA(os::FcgiNamedPipeNameEnvVariable, con.path.c_str() ))
				throw fastcgi::fastcgi_error(::GetLastError(), "Setup '%s' environment variable failed.", os::FcgiNamedPipeNameEnvVariable);
		}
		
		{
			char buff[32];
			const char *handleStr = itoa ((int) os::ShutdownEvent, buff, 10);
			if (0 == ::SetEnvironmentVariableA(os::ShutdownEventEnvVarName, handleStr ))
				throw fastcgi::fastcgi_error(::GetLastError(), "Setup '%s' environment variable failed.", os::ShutdownEventEnvVarName);
		}
	    	    
		// FastCGI on NT will set the listener pipe HANDLE in the stdin of
		// the new process.  The fact that there is a stdin and NULL handles
		// for stdout and stderr tells the FastCGI process that this is a
		// FastCGI process and not a CGI process.
		StartupInfo.dwFlags = STARTF_USESTDHANDLES;
	    
		StartupInfo.hStdInput  = con.sid.fileHandle;
		StartupInfo.hStdOutput = INVALID_HANDLE_VALUE;
		StartupInfo.hStdError  = INVALID_HANDLE_VALUE;
		
		// Make the listener socket inheritable.
		BOOL success = ::SetHandleInformation(StartupInfo.hStdInput, HANDLE_FLAG_INHERIT, TRUE);
		if(!success)
			throw fastcgi_error(::GetLastError(), "spawnWorkerProcess: SetHandleInformation for named pipe failed.");
		
		aconnect::string fullCommandLine (execPath);
		if (commandLine)
			fullCommandLine += " " + aconnect::string(commandLine);

		success = ::CreateProcessA(execPath,	// LPCSTR address of module name
				const_cast<char*> (fullCommandLine.c_str()), // LPCSTR address of command line
				NULL,			// Process security attributes
				NULL,			// Thread security attributes
				TRUE,			// Inheritable Handes inherited.
				CREATE_NO_WINDOW, // DWORD creation flags
				NULL,			// Use parent environment block
				NULL,			// Address of current directory name
				&StartupInfo,   // Address of STARTUPINFO
				&pInfo);		// Address of PROCESS_INFORMATION
	    
		if(success)
		{
			boost::mutex::scoped_lock lock (_registrationMutex);
			
			ApplicationConnection &con = _connectionsList[descriptor];
			con.pid = pInfo.hProcess;

			return con.pid;
		}
		
		throw fastcgi_error(::GetLastError(), "spawnWorkerProcess: CreateProcess failed.");
	}

	//==================================================================//
	void ApplicationManager::terminateApplications()
	{
		boost::mutex::scoped_lock lock (_registrationMutex);

		size_t instancesCount = _connectionsList.size();

		boost::scoped_array<HANDLE> handles (new HANDLE[instancesCount]);

		// signal termination event
		assert (os::ShutdownEvent != INVALID_HANDLE_VALUE);
		
		if (0 == ::SetEvent (os::ShutdownEvent))
			if (_log) _log->error("FASTCGI: terminateApplications - SetEvent for shutdown event failed, %s", 
				fastcgi_error::formatErrorMessage(::GetLastError()).c_str());

		// perform test call
		std::vector<ApplicationConnection>::iterator appIter = _connectionsList.begin();
		size_t index = 0;

		for ( ; appIter != _connectionsList.end(); ++appIter) 
		{
			assert (appIter->sid.mode != fastcgi::UnusedConnection);
			
			try
			{
				fastcgi::SocketDescriptor sd = connectToFcgiApplication (*appIter, os::DefaultReadWriteTimeout);
				fastcgi::FcgiApplicationInfo appInfo;
				
				// send FCGI_GET_VALUES to accept connection on other side
				loadFcgiAppInfo (sd, appInfo);
				closeDescriptor (sd);
			
			} catch (fastcgi_error& err) {
				_log->warn("FASTCGI: terminateApplications failed to send FCGI_GET_VALUES, %s.", err.what());
			}

			handles[index] = appIter->pid;
			++index;
		}
		
		
		DWORD waitRes = ::WaitForMultipleObjects( (DWORD) instancesCount, (const HANDLE*) handles.get(), TRUE, os::TerminateTimeout);
		
		if	(waitRes == WAIT_TIMEOUT)
		{
			if (_log)
				_log->warn("FASTCGI: application shutdown by event timed out.");

			// terminate all
			for (index=0; index<instancesCount; ++index)
			{
				BOOL retCode = ::TerminateProcess(handles[index], 0);
				if (retCode == 0)
					if (_log) _log->error("FASTCGI: terminateApplications - TerminateProcess failed, pid: %d.", appIter->pid);
			}
			
			for (index=0; index<instancesCount; ++index)
				::CloseHandle (handles[index]);
		}

		if (0 == ::ResetEvent (os::ShutdownEvent))
			if (_log) _log->error("FASTCGI: terminateApplications - ResetEvent for shutdown event failed, %s", 
				fastcgi_error::formatErrorMessage(::GetLastError()).c_str());

		_connectionsList.clear();
	}
	//==================================================================//

	void ApplicationManager::initFastCgiEnvironment() throw (fastcgi_error)
	{
		os::ShutdownEvent = ::CreateEventA(NULL, 
			TRUE, 
			FALSE,
			os::ShutdownEventName);

		if (NULL == os::ShutdownEvent)
			throw fastcgi_error(::GetLastError(), "initFastCgiEnvironment: shutdown event creation failed.");

		if(!::SetHandleInformation(os::ShutdownEvent, HANDLE_FLAG_INHERIT, TRUE))
			throw fastcgi_error(::GetLastError(), "initFastCgiEnvironment: SetHandleInformation for shutdown event failed.");

	}
	//==================================================================//

} // namespace fastcgi

#endif // WIN32

